package com.lifengdi.qiankun.common.lock;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.lang.management.ManagementFactory;
import java.net.NetworkInterface;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 基于redis的分布式锁
 *
 * @author: 李锋镝
 * @date: 2020-11-24 17:11
 */
@Component
public class DistributedLock {

    /**
     * 释放锁lua脚本
     */
    private final static String RELEASE_LOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    private final static Long RELEASE_SUCCESS_RESULT = 1L;

    private static final int MACHINE_ID;

    private static final short PROCESS_ID;

    private static final int LOW_ORDER_THREE_BYTES = 0x00ffffff;

    /**
     * 获得锁有效时间，默认1分钟
     */
    @Value("${lock.timeout:60000}")
    private long lockTimeout;

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    /**
     * lua脚本可以指定为String，也可以加载某个脚本文件的内容
     */
    private DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>(RELEASE_LOCK_LUA_SCRIPT, Long.class);

    private static final ThreadLocal<Thread> DAEMON_THREAD_LOCAL = new ThreadLocal<>();

    static {
        try {
            MACHINE_ID = createMachineIdentifier();
            PROCESS_ID = createProcessIdentifier();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public String applicationId() {
        return MACHINE_ID + "_" + PROCESS_ID + "_";
    }

    private static short createProcessIdentifier() {
        short processId;
        try {
            String processName = ManagementFactory.getRuntimeMXBean().getName();
            if (processName.contains("@")) {
                processId = (short) Integer.parseInt(processName.substring(0, processName.indexOf('@')));
            } else {
                processId = (short) java.lang.management.ManagementFactory.getRuntimeMXBean().getName().hashCode();
            }

        } catch (Throwable t) {
            processId = (short) new SecureRandom().nextInt();
        }

        return processId;
    }

    private static int createMachineIdentifier() {
        int machinePiece;
        try {
            StringBuilder sb = new StringBuilder();
            Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
            while (e.hasMoreElements()) {
                NetworkInterface ni = e.nextElement();
                sb.append(ni.toString());
                byte[] mac = ni.getHardwareAddress();
                if (mac != null) {
                    ByteBuffer bb = ByteBuffer.wrap(mac);
                    try {
                        sb.append(bb.getChar());
                        sb.append(bb.getChar());
                        sb.append(bb.getChar());
                    } catch (BufferUnderflowException shortHardwareAddressException) { //NOPMD
                        // mac with less than 6 bytes. continue
                    }
                }
            }
            machinePiece = sb.toString().hashCode();
        } catch (Throwable t) {
            machinePiece = (new SecureRandom().nextInt());
        }
        machinePiece = machinePiece & LOW_ORDER_THREE_BYTES;
        return machinePiece;
    }


    /**
     * 尝试获取锁，立即返回结果
     *
     * @param key 锁key
     * @return boolean
     */
    public boolean tryLock(String key) {
        if (ObjectUtils.isEmpty(key)) {
            return true;
        }
        return lock(key, lockTimeout, TimeUnit.MILLISECONDS);
    }

    /**
     * 尝试获取锁，立即返回结果
     *
     * @param key         锁key
     * @param autoRenewal 是否自动续约
     * @return boolean
     */
    public boolean tryLock(String key, boolean autoRenewal) {
        if (ObjectUtils.isEmpty(key)) {
            return true;
        }
        return lock(key, lockTimeout, TimeUnit.MILLISECONDS, autoRenewal);
    }

    /**
     * 尝试获取锁，如果成功，立即返回，如果一直失败，等到超时之后返回
     *
     * @param key     锁key
     * @param timeout 等待时间
     * @param unit    TimeUnit
     * @return boolean
     * @throws InterruptedException InterruptedException
     */
    public boolean tryLock(String key, long timeout, TimeUnit unit) throws InterruptedException {

        long nanoTimeout = unit.toNanos(timeout);

        long lastTime = System.nanoTime();
        do {
            if (tryLock(key)) {
                return true;
            }

            long now = System.nanoTime();
            nanoTimeout -= now - lastTime;
            lastTime = now;

            // 响应中断
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
        } while (nanoTimeout <= 0L);

        return false;
    }

    /**
     * 尝试获取锁，如果成功，立即返回，如果一直失败，等到超时之后返回
     *
     * @param key         锁key
     * @param timeout     等待时间
     * @param unit        TimeUnit
     * @param autoRenewal 是否自动续约
     * @return boolean
     * @throws InterruptedException InterruptedException
     */
    public boolean tryLock(String key, long timeout, TimeUnit unit, boolean autoRenewal) throws InterruptedException {
        boolean lock = tryLock(key, timeout, unit);
        if (lock && autoRenewal) {
            // 成功获取到锁，则设置守护线程
            autoRenewal(key, lockTimeout, TimeUnit.MILLISECONDS);
        }
        return lock;
    }

    /**
     * 尝试获取锁，立即返回结果，指定锁过期时间
     *
     * @param key         key
     * @param time        锁过期时间
     * @param unit        时间单位
     * @param autoRenewal 是否自动续约
     * @return boolean
     */
    public boolean lock(String key, long time, TimeUnit unit, boolean autoRenewal) {
        boolean lock = lock(key, time, unit);
        if (lock && autoRenewal) {
            // 成功获取到锁，则设置守护线程
            autoRenewal(key, time, unit);
        }
        return lock;
    }

    /**
     * 尝试获取锁，立即返回结果，指定锁过期时间
     *
     * @param key  key
     * @param time 锁过期时间
     * @param unit 时间单位
     * @return boolean
     */
    public boolean lock(String key, long time, TimeUnit unit) {
        long lockTime = unit.toMillis(time);
        if (setIfAbsent(key, lockTime)) {
            return true;
        }
        String currentValue = redisTemplate.opsForValue().get(key);

        // 如果currentValue为空，说明锁拥有者已经释放掉锁了，可以进行再次尝试
        if (ObjectUtils.isEmpty(currentValue)) {
            return setIfAbsent(key, lockTime);
        }

        // 判断上一个锁是否到期，到期尝试获取锁
        if (System.currentTimeMillis() >= getExpireTime(currentValue)) {
            // 多个线程恰好都到了这里，但是只有一个线程的设置值和当前值相同，它才有权利获取锁
            String oldValue = redisTemplate.opsForValue().getAndSet(key, getCacheValue(lockTime));
            return currentValue.equals(oldValue);
        }
        return false;
    }

    private boolean setIfAbsent(String key, long lockTime) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, getCacheValue(lockTime));
        return Objects.nonNull(result) && result;
    }


    /**
     * 获取锁，如果成功，立即返回，如果失败，则不停的尝试，直到成功为止
     *
     * @param key 锁key
     * @throws InterruptedException InterruptedException
     */
    public void lock(String key) throws InterruptedException {
        while (true) {
            if (tryLock(key)) {
                return;
            }
            // 响应中断
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
        }
    }

    /**
     * 尝试获取锁(指定value)，立即返回结果，指定锁过期时间
     *
     * @param key   key
     * @param value value
     * @param time  过期时间
     * @param unit  时间单位
     * @return true：成功获取到锁
     */
    public boolean lock(String key, String value, long time, TimeUnit unit) {
        return lock(key, value, time, unit, false);
    }

    /**
     * 尝试获取锁(指定value)，立即返回结果，指定锁过期时间
     *
     * @param key         key
     * @param value       value
     * @param time        过期时间
     * @param unit        时间单位
     * @param autoRenewal 是否自动续约
     * @return true：成功获取到锁
     */
    public boolean lock(String key, String value, long time, TimeUnit unit, boolean autoRenewal) {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        Boolean result = ops.setIfAbsent(key, value, time, unit);
        boolean lock = Objects.nonNull(result) && result;
        if (lock && autoRenewal) {
            // 成功获取到锁，则设置守护线程
            autoRenewal(key, time, unit);
        }
        return lock;
    }

    public void printTtl(String key) {
        System.out.println("过期时间：" + redisTemplate.getExpire(key));
    }

    /**
     * 对锁自动续约
     *
     * @param key  key
     * @param time 过期时间
     * @param unit 时间单位
     */
    public void autoRenewal(String key, long time, TimeUnit unit) {
        // 清空老的线程（若存在）
        removeDaemon();
        // 开启新的守护线程
        Thread daemon = new Thread(() -> {
            try {
                long sleep = (long) (unit.toMillis(time) * 0.8);
                while (true) {
                    Thread.sleep(sleep);
                    renewalLock(key, time, unit);
                }
            } catch (InterruptedException e) {
            }
        });
        daemon.setDaemon(true);
        daemon.start();
        DAEMON_THREAD_LOCAL.set(daemon);
    }

    /**
     * 延长锁时间
     *
     * @param key  key
     * @param time 过期时间
     * @param unit 时间单位
     * @return boolean
     */
    public boolean renewalLock(String key, long time, TimeUnit unit) {
        Boolean result = redisTemplate.expire(key, time, unit);
        return Objects.nonNull(result) && result;
    }

    /**
     * 释放锁（使用lua脚本）
     * <p>获取锁对应的value值，检查是否与传入的value值相等，如果相等则删除锁（释放锁）</p>
     *
     * @param key   key
     * @param value value
     * @return true：成功释放锁
     */
    public boolean unlock(String key, String value) {
        removeDaemon();
        Long result = redisTemplate.execute(defaultRedisScript, Collections.singletonList(key), value);
        return RELEASE_SUCCESS_RESULT.equals(result);
    }

    private void removeDaemon() {
        Thread thread = DAEMON_THREAD_LOCAL.get();
        if (Objects.nonNull(thread)) {
            thread.interrupt();
        }
        DAEMON_THREAD_LOCAL.remove();
    }

    /**
     * 释放锁
     *
     * @param key key
     */
    public void unlock(String key) {
        removeDaemon();
        redisTemplate.delete(key);
    }

    /**
     * 获取当期线程缓存value
     *
     * @param timeout 超时时间
     * @return 缓存值
     */
    private String getCacheValue(long timeout) {
        return applicationId() + "_" + Thread.currentThread().getName() + ";" + String.valueOf(System.currentTimeMillis() + timeout);
    }

    /**
     * 获取过期时间
     *
     * @param cacheValue 缓存值
     * @return 过期时间
     */
    private long getExpireTime(String cacheValue) {
        String[] values = cacheValue.split(";");
        return values.length > 1 ? Long.parseLong(values[1]) : 0L;
    }

}
